系列:「從單一 agent 到多 agent 集群,再到接進我的生活」;不設天數,第 10 篇

Day 9 花一整天證明
spectyn serve上每一條路由要嘛驗證、要嘛無害。
那個證明對「呼叫端還進不進得來」一句話都沒說。
這跟 Day 8 的主題是同一條線:東西修好了,不等於它送得到。
Day 8 是「Day 6 的每一個修法都不在操作者實際會跑的那支 binary 裡」;
今天是「門關對了,而敲門的人手上那把鑰匙我還沒逐個看過」。


Day 9 收工前跑的第三輪對抗式稽核,除了當天修掉的,留下三件成立而沒動的。
它們不是新發現的 bug,是昨天那個證明的邊界:
/costs serve.rs=0 main.rs=1 ← 只有死 daemon 有
/tools serve.rs=0 main.rs=1
/hands serve.rs=0 main.rs=1
/revenue serve.rs=0 main.rs=0 ← 兩邊都沒有
provider.rs 用 format!("{}/costs", config.hub_url) 這種寫法直接組路徑。
而 daemon_api.rs 裡有一整張 endpoint map,它的模組註解寫著這張表存在的
理由:app 當初照著錯的那個 daemon 寫,付出「後端連不上」好幾週的代價,
2026-08-06 由操作者裁定「只有一個後端」。
這四個 call site 繞過了那張表。 而繞過的方式很平凡 —— 它們不在daemon_api.rs 裡,所以那張表管不到它們。
cluster_peers(26 處提及)、health、onboarding、provider、security、skillbank_wire。
Day 9 給 daemon_api.rs 做的「一扇門」只管一個檔案。而every_request_goes_through_one_door 這條守衛掃的也只有那一個檔案 ——
又是一次「檢查它名字裡的那片」,只是這次我當天就知道,只是沒做。
而「反正都是 loopback」不成立:hub_url 是操作者可設定的,cluster_peers.rs 的 allowlist 還明確允許 *.tail.ts.net。
Err(e) if e.contains("401") => Ok(json!({ "tasks": [], "note": "unauthenticated" })),
Day 9 的文章寫了這一段,但程式還沒改。CLI 那批已經改成會簽章,
可是把「連不上」和「被拒絕」印成同一句話的顯示層一行都沒動 ——
簽章修好了,而下一次認證出問題時,它一樣不會出聲。

規則跟 Day 9 一樣:先量再修,而且量的是「誰在呼叫、打到哪、會不會簽、
出錯時使用者看到什麼」,不是「這行程式碼看起來對不對」。

Day 9 最貴的一條發現,一句話講得完:
一個只驗證「該拒的有被拒」的套件,對「該放的有沒有被放」是全盲的
—— 因為閘壞掉的時候,兩者的答案都是 401。
這條規則的價值不在它多深刻,在它便宜到可以複製。所以把它寫進工廠的WORKFLOW.md,並且——因為今天整天都在證明「寫進文件的規則會失效」——
一併寫成 scripts/paired_gate_check.sh:改動碰到閘的 PR,測試裡必須找得到
放行側的斷言。
順帶加第二道 test_count_ratchet.sh:測試總數不准變少。理由是昨天那條
被編輯吃掉 #[test] 的測試——它沒有變紅,它是不存在了,而覆蓋率也不會叫,
因為沒有人寫的測試不會降低覆蓋率。
變異是「加一行閘程式碼、只寫拒絕側測試」,而閘說「這次改動沒有碰到閘,跳過」。
追下去,先是變異本身失敗(我加的是一行註解,而註解會被過濾掉)。
但修正變異之後,發現了真的洞:
這道閘只看 diff 的 + 行。 而拆掉一道閘最直接的做法是刪掉那一行if let Err(..) = require_cluster_auth(..) —— 那種 PR 對它完全隱形。
一個「動了閘」的偵測器,看不到「把閘拿掉」。
改成新增與刪除都看,兩個變異(加閘只寫拒絕側 / 刪掉整段驗證)都紅了。
//! 文件註解。一句註解就能讓它變綠 ——最後留了逃生口(PAIRED_GATE_ACK=1,但 PR 說明要寫理由)。
沒有逃生口的閘會被整個關掉,那比誤報更糟。

七個 agent 平行量,每條 high/medium 再交給獨立駁斥者。38 條撐過駁斥。
| 路徑狀態 | 條數 |
|---|---|
| 哪裡都不存在 | 20 |
只在 main.rs(沒人跑的那個 daemon) |
4 |
| 打得到活著的路由 | 8 |
| 外部服務(broker / supabase / ollama) | 4 |
| 判定不了(由前端傳進來的 URL) | 2 |
簽章:27 條不簽、7 條 canonical v2、4 條走其他服務的認證。
38 條裡有 37 條的目標可以是非 loopback。
app/src-tauri/src/commands/goals.rs 有 18 個 Tauri 指令,全部打{hub_url}/goals/... —— 列表、建立、更新、刪除、進度、今日、摘要、里程碑、
週期任務、check-in。
"/goals" 在 serve.rs : 0
"/goals" 在 serve_skillbank : 0
"/goals" 在 main.rs : 0
三個地方都沒有。 不是打錯 daemon,是打在一個從來沒被實作的後端上。
每一條的錯誤處理都是 .error_for_status().map_err(|e| e.to_string())?,
所以使用者會拿到 HTTP status client error (404 Not Found) for url (...)。
這不是「Day 9 把門關上造成的」——它一直是壞的。Day 9 只是讓我第一次去數。
這是今天最尖的一條,因為它出現在一個安全產品的安全畫面上。
security.rs 抓 /api/governance/log(治理飛行紀錄器),用的是:
.bearer_auth(&config.auth_key)
那是第三套認證方案。 daemon 的 require_cluster_auth 只收 canonical v2 ——
bearer token 從頭到尾不會被看,結果是 401。
而 401 到畫面上變成:
(離線模式)
無法取得審計日誌(離線)— 目前無審計資料可顯示
離線中 — 無審計資料可顯示
daemon 是活的,而且它手上就握著那份稽核日誌。 畫面說它離線。
Day 9 把 /api/governance/log 關進閘裡之後,只要 hub_url 指向非 loopback,
這個畫面就永久顯示離線,而且顯示的理由是錯的。
跟昨天 daemon_tasks 那條是同一個形狀,但更糟:
那條是把 401 顯示成「沒有資料」,這條是把 401 顯示成「連不上」 ——
一個會讓人去檢查網路、重開 daemon、看 Tailscale,而問題在憑證。
38 條裡,「路徑活著 + 不簽章 + 目標可遠端」的只有兩條:cluster_peers 的 /rpc/peers、security 的 /api/governance/log。
這是好消息,也是壞消息。 好消息是 Day 9 的破壞面很小;壞消息是其餘 36 條
的問題跟 Day 9 無關,它們本來就是壞的,只是從來沒有人把送出端逐條數過。

操作者看完 Phase A,說:補後端。
那 19 個指令是一整套目標管理系統的設計 —— 目標 CRUD、里程碑、週期任務、
每日打卡、心情趨勢、週報。要蓋,大概三天。
開工之前我先讀了那個檔案為什麼長這樣。(WORKFLOW.md §8:替換一段程式碼
之前,先讀它為什麼長這樣。)
前端 Goals.tsx 的檔頭註解是這樣寫的:
這一頁取代的是 684 行、建在八個
spectyn serve從來沒有過的端點上的舊頁。
每一個都 404,而每一個呼叫點都用.catch(() => ({ goals: [] }))把錯誤吞掉 ——
所以一個完全沒有後端的頁面,畫出來的是「還沒有目標。從對話中說出你的
目標」,讀的人會以為是自己還沒設。它還畫了心情趨勢、里程碑數、每日打卡,
而那些資料沒有來源。
上一輪已經有人拆過這件事了。 而且拆得比我準:他不只發現後端不存在,
還發現這個 bug 的真正代價不是 404 —— 是它會讓使用者以為問題在自己身上。
而真正存在的東西,那段註解也寫了:~/.spectyn-mesh/goals.jsonl,一個 CLI
寫了好幾個月的 append-only ledger,四個欄位:
{tag, target, unit, window}
「每天寫 1 篇、每週運動 3 次」—— 然後 goal_deviations() 拿今天實際捕捉到的
事件去對它打分。前端早就搬過去了(goals_local_list)。
所以那 19 個指令不是壞掉的功能,是上一輪清理沒清乾淨的殘骸。
補它們等於為一個沒有使用者的介面蓋後端,然後還要處理「它跟那個真的 ledger
誰是真相」—— 兩份真相來源,一個問題,正是這兩天一直在拆的東西。
我把這個判斷交回去,操作者改成:直接刪掉。
刪之前發現它們有一套 regression 測試,20 條:
vi.mocked(invoke).mockImplementation(mockInvoke({
goals_create: goalFixture,
goals_list: [goalFixture],
...
}));
it('returns an object with an id field', async () => {
const result = await invoke('goals_create', { data: { title: 'Test goal' } });
expect(result).toHaveProperty('id');
});
它餵進去一個固定值,然後斷言那個固定值有 id。
19 個指令 404 了好幾個月,這 20 條一路全綠 —— 因為它們 mock 掉的正是壞掉的
那一層。這是「空綠」最乾淨的一個標本:測試覆蓋了一個功能的形狀,而那個功能
沒有後端。
app 端建出來的每一個 daemon 路徑,都必須在實際掛上去的路由表裡。
core/tests/the_app_only_calls_routes_that_exist.rs。它讀serve.rs + serve_skillbank.rs + 指令表推出來的路徑(Day 9 學到的:
路由表是好幾個檔案合起來的),然後掃 app 的 commands/ 裡每一個用 hub_url
組出來的路徑,對不上就紅。
:id 這種參數化路徑會做樣式比對,query 會先剝掉。刻意不管的:外部服務
(broker / supabase / ollama)、前端傳進來的 URL、以及 main.rs ——
那是沒有人跑的第二個 daemon,有另一條檢查管它。
下限斷言兩條:讀不到 80 條路由就紅、檢查不到 10 條 app 端路徑就紅 ——
一個掃描器要先證明自己看得到東西。

昨天那篇的核心句子是:
一層新 middleware 讓 53 條路由對簽章正確的遠端呼叫者回 401,
而三千六百條測試全綠。
後半句不成立。
今天對整份計畫跑前提查核時,一個 agent 去數了:apex4_phone_roundtrip.rs:77
與 test_rpc_forwarding.rs:103 本來就會用 auth_gate::signed_request 送
簽章正確的遠端請求到 /rpc/approvals/list、/rpc/inbox、/rpc/task/assign ——
而那三條都在 SELF_GATED_ROUTES 裡。
那些測試會紅。
我沒看到它們紅,是因為我從來沒跑完那次全量。而我自己的 commit 訊息結尾
就寫著:「未驗證:全量測試被存檔打斷,沒有綠燈紀錄」。
我寫過一張表,說原本六道 CI 閘對那天六個缺陷「六比零」。真正的成績是
1 抓到 / 1 有條件 / 4 漏掉:
| 缺陷 | 實際 |
|---|---|
| 53 條路由對簽對的人回 401 | nextest 會抓到 —— 我沒跑完 |
一條測試的 #[test] 被吃掉 |
只有 clippy 加 --all-targets 才會(當時沒加) |
/scan/*、三批死鑰匙、路徑洩漏、service worker |
真的四道都擋不住 |
後面那四條漏掉的原因是結構性的,值得記住:--in-diff 的 mutation 測試與
diff-cover 只看這次改動到的行,所以「這次沒改到的舊程式碼裡的缺陷」
按定義就在範圍外 —— /scan/* 那條路由是 2026-05-30 掛上去的,
CLI 那批死鑰匙放了 24 天。
順手修掉那個一個字的洞:ci-fast.yml 的 cargo clippy -- -D warnings 加上--all-targets。沒有它,clippy 不編譯 cfg(test),所以一個失去 #[test]
屬性的測試函式(會變成 dead_code 警告)完全看不到。
第二層更便宜也更難堪。第一層是一個真的認知盲點,值得寫成規則;
第二層只是規則再好,不跑也沒有用。
而這件事今天又差點重演:為了讓紅證拿到 build lock,我殺掉了跑到一半的全量。
現在的狀態仍然是沒有綠燈紀錄,而今天動了 CI、刪了一個模組、加了新測試。
38 條裡,「路徑活著 + 不簽章 + 目標可遠端」的只有兩條:cluster_peers 的 /rpc/peers、security 的 /api/governance/log。
這是好消息,也是壞消息。 好消息是 Day 9 的破壞面很小;壞消息是其餘 36 條
的問題跟 Day 9 無關,它們本來就是壞的,只是從來沒有人把送出端逐條數過。

Phase A 量出來的 38 條裡,真正需要修的那幾條有一個共同點:
它們不是壞在請求上,是壞在請求失敗之後那句話上。
security.rs 的檔頭有一段註解,寫的是上一輪修過這個 bug:
/audit在這個 daemon 上從來不存在。簽章的 pre-action gate log 在/api/governance/log,而且現在就有真實的條目 —— 所以「安全與審計」畫面
在對一個活著、而且手上正握著被要求的那份資料的 daemon 說「(離線模式)
無法取得審計日誌」。那個面板拒絕捏造條目是對的;它只是被指向了空氣。
修得很好。路徑改對了。
而 .bearer_auth(&config.auth_key) 原封不動留著。
daemon 只收一種簽章(canonical v2),bearer token 從頭到尾不會被看。
2026-09-01 之前這件事不會顯現,因為 /api/governance/log 正是那二十條
沒有閘的路由之一 —— 沒有閘的時候,錯的憑證跟對的憑證一樣好用。
Day 9 把它關進閘的那一天,「離線」就回來了。同樣的字,換了一個錯的理由。
這是今天最值得記的一種形狀:
上一輪的修法把 bug 從「指向不存在的東西」變成「指向存在但進不去的東西」,
而這兩件事在畫面上長得完全一樣。
原本的畫面是這樣:
(離線模式)
無法取得審計日誌(離線)— 目前無審計資料可顯示
離線中 — 無審計資料可顯示
三個地方都說「離線」。而實際情況是 daemon 活著、日誌就在它手上、憑證被拒。
「離線」會把人送去:檢查網路 → 重開 daemon → 看 Tailscale → 看防火牆。
全部都是對的地方以外的地方。 而正確的動作是:去 setting 分頁把
cluster 密鑰貼上去。
所以錯誤要能分辨,而不是只有「有錯」跟「沒錯」:
pub(crate) const E_REFUSED: &str = "E_REFUSED";
if status.as_u16() == 401 || status.as_u16() == 403 {
return Err(format!(
"{E_REFUSED}: {what} 被 daemon 拒絕({}) —— daemon 是通的,是憑證的問題。\
檢查這台裝置的 cluster secret,不是網路。", status.as_u16()));
}
前端據此分岔:
(憑證被拒)
審計日誌被 daemon 拒絕 — daemon 是通的,是這台裝置的憑證問題
一個錯的解釋比沒有解釋更貴。 沒有解釋的時候人會去找;有一個聽起來合理的
錯解釋時,人會照著它走完一整條死路,然後放棄。
daemon_tasks 的註解在替 401 說謊同一個形狀的第二例,而且更乾淨:
/// `/rpc/tasks/list` is HMAC-gated; when this node has no cluster_secret
/// the call comes back 401 and we surface an empty list rather than an error,
/// because "no secret configured" is not a broken daemon.
Err(e) if e.contains("401") => Ok(json!({ "tasks": [], "note": "unauthenticated" })),
那個節點有 secret。 401 的原因從來不是「沒設定密鑰」,是簽章方案在
2026-08-08 就作廢了。所以 app 的任務清單空了 24 天,而任何一個去看的人,
都會讀到那句合理的解釋,然後停在那裡。
一個錯誤處理如果對錯誤有一套解釋,而解釋是錯的,
比單純把錯誤吞掉更難發現。
單純吞掉至少什麼都沒說,人還會繼續找。有解釋的那個會主動把人擋在真相外面。
拆掉那個 match,讓錯誤原樣往上走。
/rpc/peers 也是 bearer,而它的 allowlist 允許 tailnetcluster_peers.rs 打 /rpc/peers 帶的也是 bearer token。而這個模組自己的
host allowlist 明確允許 *.tail.ts.net —— 所以「反正都是 loopback、
反正有豁免」對它從來就不成立。
三處一起改成走 daemon_api::send_signed 那扇門。Day 9 在 daemon_api.rs
裡開的那扇門,今天變成 pub(crate),給另外兩個模組用 —— 一扇門,而不是
三個各自記得要簽的地方。

Day 9 的結論是「CLI 六個指令帶著作廢的鑰匙,而且把 401 印成離線」。
簽章那半當天修了,顯示那半今天才逐條看。而逐條看的結果跟我的印象不一樣。
spectyn dispatch(派工)、spectyn msg(廣播)、spectyn git sync
(admin_shell)這三條,本來就把狀態碼和 body 一起印出來:
anyhow::bail!("RPC assign failed: HTTP {} — {}", status.as_u16(), body);
而 daemon 的 401 body 是"unauthorized — bad or missing X-Cluster-Auth / X-Cluster-Timestamp / X-Cluster-Nonce"
—— 那句話已經足夠精確。這三條沒有問題,不需要修。
真正在靜音的只有兩處,而它們一個比一個安靜。
sessions:一個回答了 401 的 peer,不是「連不上」Ok(r) => NodeRow { node: name, reachable: false, error: Some(format!("HTTP {}", ...)) }
Err(e) => NodeRow { node: name, reachable: false, error: Some("unreachable".into()) }
兩個分支都寫 reachable: false,而畫面上兩者都是 ✗。
一台回了 401 的機器是連得上的。 它開著、網路通、daemon 在跑、它讀了我們的
請求並且做了一個決定。把它跟一台關機的機器印成同一個符號,就是把
「憑證錯了」藏進「機器掛了」裡面。
改成:回答了的就 reachable: true,符號分成 ✗(沒有人回答)與⚠(有人回答了,但答案不是我們要的),而 401 的訊息直接講清楚:
z13 ⚠ HTTP 401 — 這台拒絕了我們的憑證(它是活的)。
對一下兩邊 agents.toml 的 [cluster] cluster_secret。
dispatch 的輪詢:每一次認證失敗都被印成「逾時」這一處是今天最安靜的一個 bug,而且它連錯誤處理都沒有:
let r = signed_peer_request(...).send().await?;
let body = r.text().await.unwrap_or_default();
let v = serde_json::from_str(&body).unwrap_or(Value::Null);
last_status = v.get("status").and_then(|s| s.as_str()).unwrap_or("?").to_string();
它從來沒有看過狀態碼。
401 的 body 沒有 status 欄位,所以 last_status 變成 "?",迴圈繼續輪詢,
一分鐘之後印出:
⚠ timeout after 60s. last status: ?. Continue polling: spectyn dispatch-status <id>
每一次認證失敗,都被顯示成逾時。
而「逾時」會把人送去看:那台機器是不是很忙、網路是不是慢、任務是不是卡住、
要不要把 timeout 調長。全部都是對的地方以外的地方。
改成先看狀態碼再讀 body —— 兩行的事,而它安靜了不知道多久。
三天下來,同一個形狀出現了六次:
| 在哪 | 真相 | 使用者看到 |
|---|---|---|
daemon_tasks |
憑證被拒 | 一張空的任務清單 |
| 安全稽核畫面 | 憑證被拒 | 「(離線模式)」 |
cluster_peers |
憑證被拒 | 「無法取得對等節點」 |
sessions |
憑證被拒 | ✗,跟關機的機器同一個符號 |
dispatch 輪詢 |
憑證被拒 | 「逾時 60 秒」 |
provider.rs 四條 |
路徑不存在 | 404 的原始字串 |
沒有一個是「錯誤沒有被處理」。每一個都被處理了 —— 處理成錯的東西。
一個把認證失敗顯示成別的東西的介面,等於把自己的認證錯誤靜音。
而如果它還附上一個聽起來合理的理由,它就不只是靜音,是主動把人擋在真相外面。

provider.rs 那四條打「沒人跑的 daemon」的路徑,追下去發現一件更難堪的事:
daemon_api 裡那張 endpoint map 對它們每一個早就有答案。
"cost" | "costs" => Some("/api/cost"),
"tools" => Some("/api/tools/history"),
"revenue" | "estop" | "hands" | "scores" => None, // 這個 daemon 沒有這個東西
那張表存在的理由寫在它自己的模組註解裡:app 當初照著錯的那個 daemon 寫,
付出「後端連不上」好幾週的代價,2026-08-06 由操作者裁定「只有一個後端」。
而那四個 call site 繞過了它。 繞過的方式很平凡 —— 它們不在daemon_api.rs 裡,所以那張表管不到它們。
一張表如果只約束跟它住在同一個檔案裡的人,它就不是一張表,是一個習慣。
處理:
| 指令 | 前端呼叫者 | 表怎麼說 | 做法 |
|---|---|---|---|
get_costs |
2 | /api/cost |
走表 + 簽章 |
get_tools |
1 | /api/tools/history |
走表 + 簽章 |
get_hands |
2 | None |
走表,而表誠實地說「這個 daemon 沒有這個面」 |
get_revenue |
0 | None |
刪 |
get_estop_status |
0 | None |
刪 |
get_hands 那條值得單獨講:它有兩個前端呼叫者,所以不能直接刪。但那張表說None,所以正確的行為不是「試著打某個看起來像的路由」,是回一句實話:
這個 daemon 沒有 hands 這個面 —— 不是連線問題,是這個功能在
spectyn serve上不存在。
memory.rs 的 /memory/observations 同理,而且我刻意沒有把它改指到
endpoint map 的 "memory" => /rpc/recall:那是不同的東西(語意召回,
不是觀察清單)。把一個 404 換成一個形狀不對的 200,比 404 更難發現 ——
那正是今天一整天在拆的形狀。留成 404,由 CI 每次指名它,並列成裁定點。

寫完那條 ratchet(「app 建出來的每個 daemon 路徑,必須在實際掛上去的路由表裡」)
之後跑紅證,它紅了 —— 但紅在下限斷言上:
只檢查到 1 條 app 端路徑 —— 掃描沒有看到它以為在看的東西
原因:那條掃描找的是「跟 hub_url 寫在同一行的字面路徑」。
而我剛剛把所有 call site 都改成走 daemon_route("costs") —— 路徑不再跟hub_url 出現在同一行了。
我的修法,把用來守這件事的檢查弄瞎了。 而且如果沒有那條下限斷言,
它會報綠,一路綠下去,而它其實只看到一條。
這是這十天裡「下限斷言」第二次救場。第一次是 Day 9 的一扇門守衛
(「至少要看到三個呼叫點」抓到它只掃了三分之一的檔案);這次是它自己。
一個掃描器要先證明自己看得到東西 —— 而它「看得到多少」會隨著程式碼的形狀
改變,所以那個下限本身也是會過期的。
修法不是把掃描器改複雜,是換一個更小也更真的問題:
既然現在所有 call site 都走那張表,那就檢查那張表。
the_endpoint_map_only_promises_paths_that_exist:endpoint map 承諾的每一條
路徑,都必須在活著的路由表裡。一份清單,一個地方。原本那條 call-site 掃描
留著,但它的工作改成抓繞過那張表的人,下限也跟著改成「至少一條」——
一個掃描器的下限應該描述「它現在該看到多少」,而不是「它以前看到多少」。
鐵人賽到今天第十天。這十天沒有一天是「規劃某個功能然後把它做出來」——
回頭看,做的事只有一件,重複了十次:去量一件從來沒有被量過的東西,
然後修掉量出來的結果。

| Day | 量了什麼 | 量出來的東西 |
|---|---|---|
| 1–3 | 為什麼要自己養一支 AI 艦隊、四支商用 CLI 在一台 Mac 上 | 架構與定位 |
| 4 | CLI 不准用猜的 | --config 被靜默忽略、doctor 謊報身分 |
| 5 | 兩支 AI 交叉複查一份 diff | review gate;夜間終端 agent |
| 6 | 把 CLI 真的塞進 app 的全生命週期 | 權限批准往返、同根寫入競爭、五個階段 |
| 7 | 先量再修 | 五個題目裡有三個題目本身寫錯了 |
| 8 | 交付 | Day 6 的每一個修法,都不在操作者實際會跑的那支 binary 裡 |
| 9 | 22 條無憑證路由 | 關上;然後對自己的成果稽核,17 條成立 |
| 10 | 送出端 | 38 條裡只有 8 條打得到活著的路由 |
Day 8 之後,主題就固定下來了。
Day 8 問的是「做好的東西送到了嗎」,答案是沒有。
Day 9 問的是「門關上了嗎」,答案是關上了、但關的時候我自己弄壞了一個。
Day 10 問的是「門後面的東西還連得上嗎」,答案是三十八分之八。
這三天連起來是同一句話:證明一件事情做完了,跟證明它在使用者手上還是那樣,
是兩件完全不同的事。 而我這三天做的每一次量測,都是在第二件事上。

母規則(任何檢查先示範會紅才算數)是這個專案的老規矩。但 Day 9 我寫了
「53 條路由壞掉,而三千六百條測試全綠」,今天被一輪事實查核推翻 ——
那些測試裡有幾條本來就會紅,是我沒跑完全量就提交。
規則沒有錯。是我沒有跑。 而這件事今天又差點重演。
「清點器只清點它名字裡的那片」在 Day 9 出現了四次:只量 /api、
只量兩個前綴、只認整行開頭的 .route(、只讀一個檔案。
每一次修完我都以為這次是全部了。
而今天寫「成對規則」那道閘的時候,我在它裡面犯了它要擋的那個錯:
放行側的證據裡混進一條文件註解,而「被無關文字滿足的斷言」正是這整套規則
存在的理由。
daemon_tasks 的註解說 401 是「這台沒有設 cluster_secret」。那台有 secret。dispatch 的輪詢把 401 印成逾時。sessions 把 401 印成關機。
沒有一個是「錯誤沒被處理」。每一個都被處理了,處理成錯的東西。
而錯的解釋比沒有解釋更貴 —— 沒有解釋的時候人會繼續找,有解釋的時候人會
照著它走完一整條死路。

/hands、/estop 與 provider bypass 已處理,不再列為待辦。trust_tailnet_peers;memory 三條 caller 攬哪個活契約;禁網路的本地模型主題不變:繼續把「沒有人量過的東西」量掉。 排隊中的:
這十天讓我對「接下來要做什麼」的看法變了一次,值得寫下來。
原本的路線圖是往功能長:治理層 → Skill SDK → 五平台。
而這三天量到的東西說,已經寫出來的東西正在以肉眼看不見的速度爛掉 ——
20 條打不存在路徑的請求、19 個沒有後端也沒有呼叫者的指令、
3 批用作廢簽章方案的送出端(24 天)、一個沒人跑的第二 daemon。
一個只會產出新功能的流程,會用今天的速度製造明天的這批。
所以接下來要加的不是功能,是一條拆除車道:每週固定一次「量一件從來
沒有量過的事」。這三天最有價值的三個發現全部來自清點 —— 數路由、數送出端、
數測試裡的斷言方向。不是靈感,是數數。
而那件事本身可能就是這個專案最值錢的東西。它三天內在一份「已經做完」的
程式碼裡找出五十幾條成立的問題,其中包括作者當天寫的,以及它自己
寫錯的那一句。一個只會找別人 bug 的方法不稀奇;會在同一天內拆掉作者當天
成果的方法,才是可以交給別人用的。

上一輪已經讓後端錯誤保留 E_REFUSED,但畫面還有一個很小、也很實際的 correctness bug:
isRefused=true、isOffline=false。isOffline,所以拒絕說明永遠不可達。isOffline,拒絕時只剩一般空資料訊息。這次把 banner 條件改成 isOffline || isRefused,空表則先判斷 isRefused。新增的 regression test 讓 get_audit_log 拒絕 E_REFUSED: HTTP 401,並固定四件事:拒絕 banner 可見、空表明說憑證被拒、不出現離線空表、不出現「離線模式」。
精準前端驗證:PASS(SecurityPanel.test.tsx:4 passed,exit 0)。
Rust caller/route baseline 這次沒有完成。同一個 core/target 已經有另一個先前啟動、超過一小時仍未結束的 cargo nextest;本輪 targeted Cargo test 編譯超過六分鐘後中止,exit 130,後面的 paired gate 與 count ratchet 沒有執行。這一段不能寫成綠燈。
| Phase | 狀態 | 還缺什麼 |
|---|---|---|
| A caller inventory | 部分完成 | 38 條 inventory 已分類;memory 三條 caller 尚未決定活契約或移除,原 Phase A DoD 尚未達成 |
| B dead path removal | 部分完成 | provider/hands/estop/revenue 已處理;memory 三條刻意 404 仍待產品裁定,route ratchet 因此尚未收口 |
| C one request door | 部分完成 | 多數 caller 已走 endpoint map + signed request;memory 還有 direct bearer,全 commands 掃描與 signed positive tests 尚未完成 |
| D rejection semantics | 部分完成 | CLI 與多個 command 已分開 refused/offline;本輪修正 SecurityPanel 的不可達拒絕正文,仍需全 UI 401 ratchet |
| E Day 9 tail | 未完成 | mobile credential、第二 daemon 與 medium audit 保留原 Phase E 定義,均待 owner/gate |
| 發布證據(非 Phase) | 進行中 | 不能在 Rust gate 與 owner decisions 未結束前宣稱 Day 10 全綠 |
還有四個必須由 owner 明文裁定的邊界:
main.rs 的第二 daemon 是保留、合併,還是刪除。
**PROPOSAL / AWAITING OWNER:**以下是根據目前 repo、設備與訂閱限制提出的執行建議,不是已裁決的產品 SSOT。
最大化的單位不是 token、同時跑幾個 agent,也不是六台機器的使用率。應該量:
每一小時人類決策時間,產生多少通過驗收、隔天仍可使用的成果。
如果 owner 接受這個方向,建議分工如下:
| 資源 | 固定車道 | 不做 |
|---|---|---|
| 人腦 | 每天唯一優先序、驗收條件、風險裁決、dogfood、客戶訪談 | 不同時救三條產品線,不替 agent 猜模糊需求 |
| Codex | 主實作、整合、長任務、CI 修復;每張卡只有一個 DoD | 不與另一 worker 同改核心檔;沒有 timeout/budget 不過夜 |
| Claude Code | Tier A 架構、威脅模型、安全 review、難 Rust | 不當 24/7 daemon,不握 release secret |
| Antigravity / Jules | 獨立測試、文件、低耦合 GitHub 背景任務 | 不與 Codex 同檔競寫,不進簽章與 production |
| Z13 本地模型 | 離線 eval、公開或合成資料的批次分類、低成本 janitor | 私人資料需 owner 明文允許;不直接 merge、deploy 或寫正式 memory |
| 免費雲端模型 | 公開程式碼的 Tier C/D:測試候選、文件、分類 | 不放 secret、私人 memory、客戶資料或 critical path |
一張 task 只設一個 primary worker 與一個 reviewer。只有金鑰、資料刪除、安全邊界、release 才升級 Claude + Codex 雙檢。所有模型用實際一次通過率、修復次數、人類審核分鐘數動態路由,不用品牌印象排優先級。
設備建議採固定車道:
官方工具可當 operator control plane,而不是產品 dependency:Codex 官方 use cases 涵蓋 goal-following、project teammate 與文件維護;深度安全掃描需要另行安裝 Codex Security plugin。Claude Remote Control 連線期間的完整 transcript、訊息、回應與 tool activity 會儲存在 Anthropic 伺服器;Antigravity 的 retention 邊界本輪尚未確認,兩者都不承載未經允許的私人內容。Jules 適合 bounded GitHub background task。這些工具的額度與 preview 行為會變,也不能寫進產品 SLA。

出口:14 天至少 10 天真用;連續 7 天有非空真實事件;RB102 三次 clean install + rollback;沒有匿名 privileged route;runner 讀不到簽章私鑰。
出口:每週使用至少 5 天;每個過夜任務都有 budget/timeout/kill/ledger;手機 console 在 14 天內至少 10 天完成 status/stop/approve 且零次繞過 auth;至少一筆非零 pilot 訂金實際入帳。
建議將 v1.0 範圍縮到:單機完整可用、兩機派工、OS keystore、最小 governor、signed installer、單一手機 console、可驗證復原。
若 owner 決定替「L0–L5 自主度」改名,建議採 AUT0–AUT5,避免同時撞上 repo lifecycle L0/L1 與本文 Phase A–E。
只有 owner 接受本提案後,46–120 天才採用以下 GO/NO-GO:
usage.jsonl。以上證據集中寫入 docs/releases/v1.0.0.md,引用 CI/nightly URL、flight-recorder run id、usage.jsonl 摘要與 store submission 紀錄;缺一項就是 NO-GO。

若 owner 接受此方向,商店架構建議為:可執行邏輯在使用者自有 backend,手機只收送資料、顯示狀態與代為執行明確授權的裝置能力。不要把下載 executable skill、手機 24/7 常駐 agent 或訂閱 OAuth 轉接放進 critical path。